package com.abso.sunlight.explorer.editors;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.BaseLabelProvider;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IReusableEditor;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.EditorPart;

import com.abso.sunlight.api.Legislator;
import com.abso.sunlight.explorer.CommandUtils;
import com.abso.sunlight.explorer.CongressExplorerPlugin;
import com.abso.sunlight.explorer.LegislatorSearch;
import com.abso.sunlight.explorer.SelectionUtils;
import com.abso.sunlight.explorer.events.ISearchStateListener;
import com.abso.sunlight.explorer.events.SearchStateChangedEvent;

/**
 * The list editor shows in a table a list of legislators as returned by a specific search, allowing to reorder the list using columns'
 * headers and export the data as PDF or Excel.
 */
public class LegislatorListEditor extends EditorPart implements IReusableEditor, ISearchStateListener {

    /** The editor's unique identifier. */
    public static final String ID = LegislatorListEditor.class.getName();

    /** Column's properties. */
    enum Property {
        TITLE("Title"), FIRST_NAME("First Name"), MIDDLE_NAME("M."), NICKNAME("Nick."), LAST_NAME("Last Name"), SUFFIX("S."), GENDER(
                "Gender", SWT.CENTER), PARTY("Party"), IN_OFFICE("In Office"), STATE("State"), DISTRICT("District", SWT.RIGHT), BIOGUIDE_ID(
                "Bioguide ID", SWT.RIGHT);

        /** The column's label. */
        private String label;

        /** Text alignment. */
        private int alignment;

        /**
         * Constructs a new property, with a predefined left alignment.
         * 
         * @param label
         *            the column's label.
         */
        Property(String label) {
            this(label, SWT.LEFT);
        }

        /**
         * Constructs a new property.
         * 
         * @param label
         *            the column's label.
         * @param alignment
         *            the alignment.
         */
        Property(String label, int alignment) {
            this.label = label;
            this.alignment = alignment;
        }
    }

    /** The table viewer displaying the list of legislators. */
    private TableViewer tableViewer;

    /** The list of sorted columns. */
    private List<PropertySort> sortList;

    /** Constructs a new editor. */
    public LegislatorListEditor() {
        this.sortList = new ArrayList<PropertySort>();
    }

    @Override
    public void init(IEditorSite site, IEditorInput input) throws PartInitException {
        setSite(site);
        setInput(input);
        setPartName(input.getName());
        CongressExplorerPlugin.getSearchManager().addSearchStateListener(this);
    }

    @Override
    public void setInput(IEditorInput input) {
        super.setInput(input);
        setPartName(input.getName());
    }

    /**
     * Returns the search held by this editor.
     * 
     * @return the search, wrapping both the query and the results.
     */
    public LegislatorSearch getSearch() {
        LegislatorListEditorInput input = (LegislatorListEditorInput) getEditorInput();
        return input.getSearch();
    }

    /**
     * Returns the list of all displayed legislators, aligned with the current sorting.
     * 
     * @return the ordered list of legislators.
     */
    public List<Legislator> getSortedLegislators() {
        List<Legislator> legislators = new ArrayList<Legislator>(getSearch().getResult().size());
        int size = tableViewer.getTable().getItemCount();
        for (int i = 0; i < size; i++) {
            Object obj = tableViewer.getElementAt(i);
            if (obj instanceof Legislator) {
                legislators.add((Legislator) obj);
            }
        }
        return legislators;
    }

    /**
     * Returns the list of the selected legislators, aligned with the current sorting.
     * 
     * @return the ordered list of selected legislators.
     */
    public List<Legislator> getSortedSelectedLegislators() {
        Set<Legislator> selectedLegislators = new HashSet<Legislator>(SelectionUtils
                .getSelectedLegislators(tableViewer.getSelection()));
        List<Legislator> legislators = new ArrayList<Legislator>(getSearch().getResult().size());
        int size = tableViewer.getTable().getItemCount();
        for (int i = 0; i < size; i++) {
            Object obj = tableViewer.getElementAt(i);
            if (obj instanceof Legislator) {
                if (selectedLegislators.contains(obj)) {
                    legislators.add((Legislator) obj);
                }
            }
        }
        return legislators;
    }

    @Override
    public boolean isDirty() {
        return !getSearch().isSaved();
    }

    @Override
    public void doSave(IProgressMonitor monitor) {
        Map<String, Object> contextParams = new HashMap<String, Object>();
        contextParams.put("activeEditorId", LegislatorListEditor.ID);
        contextParams.put("activePartId", LegislatorListEditor.ID);
        contextParams.put("activeEditor", this);
        CommandUtils.executeCommand("org.eclipse.ui.file.save", contextParams, getSite().getShell());
    }

    @Override
    public void doSaveAs() {
    }

    @Override
    public boolean isSaveAsAllowed() {
        return false;
    }

    @Override
    public void createPartControl(Composite parent) {
        Color bgColor = parent.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND);
        Color fgColor = parent.getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND);

        /* adds the main area */
        Composite mainArea = new Composite(parent, SWT.NONE);
        mainArea.setLayout(new GridLayout(1, true));
        mainArea.setBackground(bgColor);

        /* adds the title label */
        Label countLabel = new Label(mainArea, SWT.NONE);
        countLabel.setFont(JFaceResources.getHeaderFont());
        countLabel.setBackground(bgColor);
        countLabel.setForeground(JFaceResources.getColorRegistry().get(JFacePreferences.COUNTER_COLOR));
        countLabel.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
        int legislatorCount = getLegislators().size();
        if (legislatorCount == 1) {
            countLabel.setText("1 legislator found");
        } else {
            countLabel.setText(legislatorCount + " legislators found");
        }

        /* adds the result table */
        Table table = new Table(mainArea, SWT.FULL_SELECTION | SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
        table.setBackground(bgColor);
        table.setForeground(fgColor);
        table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        table.setLayout(new GridLayout(1, true));
        table.setHeaderVisible(true);
        table.setLinesVisible(true);
        for (Property prop : Property.values()) {
            addColumn(table, prop);
        }
        tableViewer = new TableViewer(table);
        tableViewer.setContentProvider(new ArrayContentProvider());
        tableViewer.setLabelProvider(new TableLabelProvider());
        tableViewer.setInput(getLegislators());
        MenuManager menuManager = new MenuManager();
        Menu menu = menuManager.createContextMenu(table);
        table.setMenu(menu);
        getSite().registerContextMenu(menuManager, tableViewer);
        getSite().setSelectionProvider(tableViewer);
        tableViewer.addDoubleClickListener(new IDoubleClickListener() {

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.jface.viewers.IDoubleClickListener#doubleClick(org.eclipse.jface.viewers.DoubleClickEvent)
             */
            public void doubleClick(DoubleClickEvent event) {
                CommandUtils.executeCommand("com.abso.sunlight.explorer.commands.ShowLegislatorDetailsCommand",
                        new HashMap<String, Object>(), getSite().getShell());
            }
        });
        for (TableColumn col : table.getColumns()) {
            col.pack();
        }
        sort(Property.FIRST_NAME);
        sort(Property.LAST_NAME);
    }

    @Override
    public void setFocus() {
        tableViewer.getTable().setFocus();
    }

    /**
     * Returns the list of all legislators held by this editor.
     * 
     * @return the list of all legislators.
     */
    private List<Legislator> getLegislators() {
        LegislatorListEditorInput input = (LegislatorListEditorInput) getEditorInput();
        return input.getSearch().getResult();
    }

    /**
     * Adds a table's column.
     * 
     * @param table
     *            the parent table.
     * @param prop
     *            the property displayed in the column.
     */
    private void addColumn(Table table, final Property prop) {
        int widthChars = prop.label.length();
        for (Legislator legislator : getLegislators()) {
            widthChars = Math.max(widthChars, getPropertyValue(legislator, prop).length());
        }
        TableColumn col = new TableColumn(table, prop.alignment, prop.ordinal());
        col.setText(prop.label);
        col.addSelectionListener(new SelectionListener() {

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.swt.events.SelectionListener#widgetDefaultSelected(org.eclipse.swt.events.SelectionEvent)
             */
            public void widgetDefaultSelected(SelectionEvent e) {
            }

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.swt.events.SelectionListener#widgetSelected(org.eclipse.swt.events.SelectionEvent)
             */
            public void widgetSelected(SelectionEvent e) {
                sort(prop);
            }

        });
    }

    /**
     * Invoked in response to a click on a column's header, forcing to recompute the table's sorting.
     * 
     * @param prop
     *            the property associated with the column.
     */
    private void sort(Property prop) {
        if (sortList.isEmpty()) {
            sortList.add(0, new PropertySort(prop, true));
        } else {
            PropertySort topSort = sortList.get(0);
            if (topSort.property == prop) { // reverse
                topSort.ascending = !topSort.ascending;
            } else {
                for (Iterator<PropertySort> i = sortList.iterator(); i.hasNext();) {
                    PropertySort sort = i.next();
                    if (sort.property == prop) {
                        i.remove();
                    }
                }
                sortList.add(0, new PropertySort(prop, true));
            }
        }
        tableViewer.setComparator(new TableComparator(sortList));
    }

    /**
     * Returns the legislator's data for a specific property.
     * 
     * @param legislator
     *            a legislator.
     * @param prop
     *            the property associated with a specific column.
     * @return the legislator's data.
     */
    private static String getPropertyValue(Legislator legislator, Property prop) {
        switch (prop) {
        case TITLE:
            return legislator.getTitle().toString();
        case FIRST_NAME:
            return legislator.getFirstName();
        case MIDDLE_NAME:
            return legislator.getMiddleName();
        case LAST_NAME:
            return legislator.getLastName();
        case SUFFIX:
            return legislator.getNameSuffix();
        case NICKNAME:
            return legislator.getNickname();
        case GENDER:
            String genderAbbr = legislator.getGender().getAbbreviation();
            return (genderAbbr.equals("M") || genderAbbr.equals("F")) ? genderAbbr : "";
        case IN_OFFICE:
            return legislator.isInOffice() ? "Active" : "Inactive";
        case PARTY:
            return legislator.getParty().toString();
        case STATE:
            return legislator.getState().toString();
        case DISTRICT:
            return legislator.getDistrict();
        case BIOGUIDE_ID:
            return legislator.getBioguideId();
        }
        return StringUtils.EMPTY;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.abso.sunlight.explorer.events.ISearchStateListener#searchStateChanged(com.abso.sunlight.explorer.events.SearchStateChangedEvent
     * )
     */
    public void searchStateChanged(SearchStateChangedEvent event) {
        switch (event.getType()) {
        case LEGISLATOR_SEARCH_ADDED:
            if (event.getSource() == getSearch()) { // 'Save' or 'Save as' called on unsaved search
                setInput(new LegislatorListEditorInput((LegislatorSearch) event.getSource()));
                firePropertyChange(PROP_DIRTY);
            } else if (event.getFromSearch() == getSearch()) { // 'Save as' called on saved search
                IEditorInput editorInput = new LegislatorListEditorInput((LegislatorSearch) event.getSource());
                IWorkbenchWindow window = getEditorSite().getWorkbenchWindow();
                if (window.getActivePage().findEditor(editorInput) == null) {
                    try {
                        window.getActivePage().openEditor(editorInput, LegislatorListEditor.ID, true, IWorkbenchPage.MATCH_NONE);
                    } catch (PartInitException e) {
                        CongressExplorerPlugin.getDefault().showErrorDialog(window.getShell(), "Error", "Unable to show results",
                                e.getMessage(), e);
                    }
                }
            }
            break;
        case LEGISLATOR_SEARCH_DELETED:
            if (event.getSource() == getSearch()) {
                close(false);
            }
            break;
        case LEGISLATOR_SEARCH_RENAMED:
            setPartName(getEditorInput().getName());
            break;
        }
    }

    /**
     * Closes the editor.
     * 
     * @param save
     *            saves the editor if the content is unsaved.
     */
    private void close(final boolean save) {
        Display display = getSite().getShell().getDisplay();
        display.asyncExec(new Runnable() {
            public void run() {
                getSite().getPage().closeEditor(LegislatorListEditor.this, save);
            }
        });

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.forms.editor.FormEditor#dispose()
     */
    @Override
    public void dispose() {
        super.dispose();
        CongressExplorerPlugin.getSearchManager().removeSearchStateListener(this);
    }

    /**
     * A specialized label provider for the table showing legislators.
     */
    private static final class TableLabelProvider extends BaseLabelProvider implements ITableLabelProvider {

        /*
         * (non-Javadoc)
         * 
         * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnImage(java.lang.Object, int)
         */
        public Image getColumnImage(Object element, int columnIndex) {
            return null;
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnText(java.lang.Object, int)
         */
        public String getColumnText(Object element, int columnIndex) {
            return getPropertyValue((Legislator) element, Property.values()[columnIndex]);
        }

    }

    /**
     * Objects of this class mantain a reference to a specific column's property as well as a sort policy (ascending or descending).
     */
    private static final class PropertySort {

        /** The column's property. */
        private Property property;

        /** The sort policy. */
        private boolean ascending;

        /**
         * Constructs a new sort information.
         * 
         * @param property
         *            the column's property.
         * @param ascending
         *            the sort policy.
         */
        public PropertySort(Property property, boolean ascending) {
            this.property = property;
            this.ascending = ascending;
        }

        @Override
        public String toString() {
            return property.label + " " + (ascending ? "asc" : "desc");
        }

    }

    /**
     * A specialized comparator for the table showing legislators.
     */
    private static final class TableComparator extends ViewerComparator {

        /** The list of sort information. */
        private List<PropertySort> sortList;

        /**
         * Constructs a new comparator.
         * 
         * @param sortList   the list of sort information.
         */
        public TableComparator(List<PropertySort> sortList) {
            this.sortList = sortList;
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.eclipse.jface.viewers.ViewerComparator#compare(org.eclipse.jface.viewers.Viewer, java.lang.Object,
         * java.lang.Object)
         */
        @Override
        public int compare(Viewer viewer, Object e1, Object e2) {
            Legislator legislator1 = (Legislator) e1;
            Legislator legislator2 = (Legislator) e2;
            for (PropertySort sort : sortList) {
                String value1 = getPropertyValue(legislator1, sort.property);
                String value2 = getPropertyValue(legislator2, sort.property);
                int compareValue = 0;
                if (sort.property == Property.DISTRICT) {
                    if (NumberUtils.isDigits(value1) && NumberUtils.isDigits(value2)) {
                        compareValue = NumberUtils.toInt(value1) - NumberUtils.toInt(value2);
                    } else {
                        compareValue = value1.compareTo(value2);
                    }
                } else {
                    compareValue = value1.compareTo(value2);
                }
                if (compareValue != 0) {
                    return sort.ascending ? compareValue : -compareValue;
                }
            }
            return 0;
        }

    }

}
